/*
 * @(#)TableSearchable.java
 *
 * Copyright 2002 - 2004 JIDE Software Inc. All rights reserved.
 */
package com.element.swing.search;

import com.element.util.CompareUtil;

import javax.swing.*;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

/**
 * <code>TableSearchable</code> is an concrete implementation of {@link Searchable} that enables the search function in
 * JTable. <p>It's very simple to use it. Assuming you have a JTable, all you need to do is to call
 * <code><pre>
 * JTable table = ....;
 * TableSearchable searchable = new TableSearchable(table);
 * </pre></code>
 * Now the JTable will have the search function.
 * <p/>
 * As JTable is a two dimension data, the search is a little different from JList and JTree which both have one
 * dimension data. So there is a little work you need to do in order to convert from two dimension data to one dimension
 * data. We use the selection mode to determine how to convert. There is a special property called mainIndex. You can
 * set it using setMainIndex(). If the JTable is in row selection mode, mainIndex will be the column that you want
 * search at. Please note you can change mainIndex at any time.
 * <p/>
 * On the other hand, if the JTable is in column selection mode, mainIndex will be the row that you want search at.
 * There is one more case when cell selection is enabled. In this case, mainIndex will be ignore; all cells will be
 * searched.
 * <p/>
 * In three cases above, the keys for find next and find previous are different too. In row selection mode, up/down
 * arrow are the keys. In column selection mode, left/right arrow are keys. In cell selection mode, both up and left
 * arrow are keys to find previous occurrence, both down and right arrow are keys to find next occurrence.
 * <p/>
 * In addition, you might need to override convertElementToString() to provide you own algorithm to do the conversion.
 * <code><pre>
 * JTable table = ....;
 * TableSearchable searchable = new TableSearchable(table) {
 *      protected String convertElementToString(Object object) {
 *          ...
 *      }
 * };
 * </pre></code>
 * <p/>
 * Additional customization can be done on the base Searchable class such as background and foreground color,
 * keystrokes, case sensitivity,
 */
public class TableSearchable extends Searchable implements TableModelListener, PropertyChangeListener {

	private int[] _searchColumnIndices = {0};

	public TableSearchable(JTable table) {
		super(table);
	}

	@Override
	public void installListeners() {
		super.installListeners();
		if (_component instanceof JTable) {
			((JTable) _component).getModel().addTableModelListener(this);
			_component.addPropertyChangeListener("model", this);
		}
	}

	@Override
	public void uninstallListeners() {
		super.uninstallListeners();
		if (_component instanceof JTable) {
			((JTable) _component).getModel().removeTableModelListener(this);
			_component.removePropertyChangeListener("model", this);
		}
	}

	@Override
	public void setSelectedIndex(int index, boolean incremental) {
		int majorIndex, minorIndex;
		JTable table = ((JTable) _component);
		if (isColumnSelectionAllowed(table)) {
			minorIndex = index;
			majorIndex = getMainIndex();
			addTableSelection(table, majorIndex, minorIndex, incremental);
		} else if (isRowSelectionAllowed(table)) {
			majorIndex = index;
			minorIndex = table.convertColumnIndexToView(getMainIndex());
			addTableSelection(table, majorIndex, minorIndex, incremental);
		} else { // cell selection allowed
			int columnCount = table.getColumnCount();
			if (columnCount == 0) {
				return;
			}
			majorIndex = index / columnCount;
			minorIndex = index % columnCount;
			addTableSelection(table, majorIndex, minorIndex, incremental);
		}
	}

	/**
	 * Selects the cell at the specified row and column index. If incremental is true, the previous selection will not
	 * be cleared. This method will use {@link JTable#changeSelection(int, int, boolean, boolean)} method to select the
	 * cell if the row and column index is in the range and the cell was not selected. The last two parameters of
	 * changeSelection is true and false respectively.
	 *
	 * @param table       the table
	 * @param rowIndex    the row index of the cell.
	 * @param columnIndex the column index of the cell
	 * @param incremental false to clear all previous selection. True to keep the previous selection.
	 */
	protected void addTableSelection(JTable table, int rowIndex, int columnIndex, boolean incremental) {
		if (!incremental)
			table.clearSelection();
		if (rowIndex >= 0 && columnIndex >= 0 && rowIndex < table.getRowCount() && columnIndex < table.getColumnCount()
				&& !table.isCellSelected(rowIndex, columnIndex)) {
			table.changeSelection(rowIndex, columnIndex, true, false);
		}
	}

	/**
	 * Is the column selection allowed?
	 *
	 * @param table the table.
	 * @return true if the table is the column selection.
	 */
	protected boolean isColumnSelectionAllowed(JTable table) {
		// NOTES: must sync with TableShrinkSearchableSupport#isColumnSelectionAllowed.
		return getSearchColumnIndices().length == 1 && (table.getColumnSelectionAllowed() && !table.getRowSelectionAllowed());
	}

	/**
	 * Is the row selection allowed?
	 *
	 * @param table the table.
	 * @return true if the table is the row selection.
	 */
	protected boolean isRowSelectionAllowed(JTable table) {
		// NOTES: must sync with TableShrinkSearchableSupport#isRowSelectionAllowed.
		return getSearchColumnIndices().length == 1 && (!table.getColumnSelectionAllowed() && table.getRowSelectionAllowed());
	}

	/**
	 * Are we trying to search on multi-columns (but NOT all columns)?
	 *
	 * @return true if the search is set to look at multi-columns (but NOT all columns).
	 */
	protected boolean isSearchSelectedRows() {
		return getSearchColumnIndices().length > 1;
	}

	/**
	 * Gets the selected index.
	 *
	 * @return the selected index.
	 */
	@Override
	public int getSelectedIndex() {
		JTable table = (JTable) _component;
		if (isColumnSelectionAllowed(table)) {
			return table.getColumnModel().getSelectionModel().getAnchorSelectionIndex();
		} else if (isRowSelectionAllowed(table)) {
			return table.getSelectionModel().getAnchorSelectionIndex();
		} else { // cell selection allowed
			return table.getSelectionModel().getAnchorSelectionIndex() * table.getColumnCount() + table.getColumnModel().getSelectionModel().getAnchorSelectionIndex();
		}
	}

	@Override
	public Object getElementAt(int index) {
		JTable table = (JTable) _component;
		if (isColumnSelectionAllowed(table)) { // column selection mode
			return getValueAt(table, getMainIndex(), index);
		} else if (isRowSelectionAllowed(table)) { // row selection mode
			return getValueAt(table, index, table.convertColumnIndexToView(getMainIndex()));
		} else if (isSearchSelectedRows()) { // search on multi columns
			int columnIndex = index % table.getColumnCount();
			int modelIndex = table.convertColumnIndexToModel(columnIndex);
			boolean doNotSearch = true;
			for (int i : getSearchColumnIndices()) {
				if (i == modelIndex) {
					doNotSearch = false;
					break;
				}
			}

			if (doNotSearch) {
				return null;
			}

			int rowIndex = index / table.getColumnCount();
			return getValueAt(table, rowIndex, columnIndex);
		} else { // cell selection allowed
			int columnIndex = index % table.getColumnCount();
			int rowIndex = index / table.getColumnCount();
			return getValueAt(table, rowIndex, columnIndex);
		}
	}

	/**
	 * Get string value of the table.
	 *
	 * @param table       the JTable
	 * @param rowIndex    the row index
	 * @param columnIndex the column index
	 * @return the string value of the cell in the table.
	 */
	protected Object getValueAt(JTable table, int rowIndex, int columnIndex) {
		if (rowIndex >= 0 && rowIndex < table.getRowCount() && columnIndex >= 0 && columnIndex < table.getColumnCount()) {
			return table.getValueAt(rowIndex, columnIndex);
		} else {
			return null;
		}
	}

	@Override
	public int getElementCount() {
		JTable table = ((JTable) _component);
		if (isColumnSelectionAllowed(table)) {
			return table.getColumnCount();
		} else if (isRowSelectionAllowed(table)) {
			return table.getRowCount();
		} else { // cell selection allowed
			return table.getColumnCount() * table.getRowCount();
		}
	}

	@Override
	public String convertElementToString(Object item) {
		if (item != null) {
			return item.toString();
		} else {
			return "";
		}
	}

	/**
	 * Gets the indexes of the column to be searched.
	 *
	 * @return the indexes of the column to be searched.
	 */
	public int[] getSearchColumnIndices() {
		return _searchColumnIndices;
	}

	/**
	 * Gets the index of the column to be searched.
	 *
	 * @return the index of the column to be searched.
	 */
	public int getMainIndex() {
		if (_searchColumnIndices.length == 0) {
			return -1;
		}

		return _searchColumnIndices[0];
	}

	/**
	 * Sets the main indexes. Main indexes are the columns index which you want to be searched.
	 *
	 * @param columnIndices the index of the columns to be searched. If empty, all columns will be searched.
	 */
	public void setSearchColumnIndices(int[] columnIndices) {
		if (columnIndices == null) {
			columnIndices = new int[0];
		}

		int[] old = _searchColumnIndices;
		if (!CompareUtil.equals(old, columnIndices, true)) {
			_searchColumnIndices = columnIndices;
			hidePopup();
		}
	}

	/**
	 * Sets the main index. Main index is the column index which you want to be searched.
	 *
	 * @param mainIndex the index of the column to be searched. If -1, all columns will be searched.
	 */
	public void setMainIndex(int mainIndex) {
		int[] temp = {mainIndex};
		if (mainIndex < 0) {
			temp = new int[0];
		}
		int[] old = _searchColumnIndices;
		if (old != temp) {
			_searchColumnIndices = temp;
			hidePopup();
		}
	}

	@Override
	protected boolean isFindNextKey(KeyEvent e) {
		int keyCode = e.getKeyCode();
		JTable table = ((JTable) _component);
		if (isColumnSelectionAllowed(table)) {
			return keyCode == KeyEvent.VK_RIGHT;
		} else if (isRowSelectionAllowed(table)) {
			return keyCode == KeyEvent.VK_DOWN;
		} else { // cell selection allowed
			return keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_RIGHT;
		}
	}

	@Override
	protected boolean isFindPreviousKey(KeyEvent e) {
		int keyCode = e.getKeyCode();
		JTable table = ((JTable) _component);
		if (isColumnSelectionAllowed(table)) {
			return keyCode == KeyEvent.VK_LEFT;
		} else if (isRowSelectionAllowed(table)) {
			return keyCode == KeyEvent.VK_UP;
		} else { // cell selection allowed
			return keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_LEFT;
		}
	}

	public void tableChanged(TableModelEvent e) {
		if (isProcessModelChangeEvent()) {
			hidePopup();
			fireSearchableEvent(new SearchableEvent(this, SearchableEvent.SEARCHABLE_MODEL_CHANGE));
		}
	}

	public void propertyChange(PropertyChangeEvent evt) {
		if ("model".equals(evt.getPropertyName())) {
			hidePopup();

			if (evt.getOldValue() instanceof TableModel) {
				((TableModel) evt.getOldValue()).removeTableModelListener(this);
			}

			if (evt.getNewValue() instanceof TableModel) {
				((TableModel) evt.getNewValue()).addTableModelListener(this);
			}
			fireSearchableEvent(new SearchableEvent(this, SearchableEvent.SEARCHABLE_MODEL_CHANGE));
		}
	}

	@Override
	protected boolean isActivateKey(KeyEvent e) {
		boolean editable = isSelectedCellEditable();
		return !editable && super.isActivateKey(e);
	}

	/**
	 * Checks if the selected cell is editable. If yes, we will not activate Searchable when key is typed.
	 *
	 * @return true if the selected cell is editable.
	 */
	protected boolean isSelectedCellEditable() {
		int selectedRow = ((JTable) _component).getSelectedRow();
		int selectedColumn = ((JTable) _component).getSelectedColumn();
		return selectedRow != -1 && selectedColumn != -1 && ((JTable) _component).isCellEditable(selectedRow, selectedColumn);
	}
}
